package avl.sv.server.training;

import avl.sv.server.KeyValueRef;
import avl.sv.server.PermissionsSet;
import avl.sv.server.sources.SlideSourceKVStore;
import avl.sv.server.sources.SolutionSourceKVStore;
import avl.sv.server.sources.StudySourceKVStore;
import avl.sv.shared.Annotations.AnnotationSet;
import avl.sv.shared.ImageID;
import avl.sv.shared.Permissions;
import avl.sv.shared.SampleSetClass;
import avl.sv.shared.SlideReference;
import avl.sv.shared.SlideSets;
import avl.sv.shared.Solution;
import avl.sv.shared.XML.SlideSetsXML_Writer;
import avl.sv.shared.ClassifierClass;
import avl.sv.server.PasswordHash;
import avl.sv.shared.sources.SlideSource;
import java.beans.XMLEncoder;
import java.io.ByteArrayOutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.servlet.http.HttpServletRequest;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.handler.MessageContext;
import oracle.kv.KVStore;
import oracle.kv.Key;
import oracle.kv.Value;
import oracle.kv.ValueVersion;

@WebService(serviceName = "Training")
public class Training {
    
    @Resource
    private WebServiceContext wsContext;

    public String getSessionID() {
        String userID = null;
        final Object sr = wsContext.getMessageContext().get(MessageContext.SERVLET_REQUEST);
        if ((sr != null) && (sr instanceof HttpServletRequest)) {
            final HttpServletRequest httpServletRequest = (HttpServletRequest) sr;
            Object attr = httpServletRequest.getAttribute("javax.servlet.request.ssl_session_id");    
            if (attr instanceof String){
                userID = (String)attr;
            }                                        
        } 
        return userID;
    }
    
    private TrainingSessionManager sessionManager = TrainingSessionManager.getInstance() ;
        
    @WebMethod(operationName = "getAnnotations")
    public String getAnnotations( @WebParam(name = "studyID") final int studyID,
                                  @WebParam(name = "slideSetName") final String slideSetName,
                                  @WebParam(name = "slideName") final String slideName,
                                  @WebParam(name = "hash") final byte[] hash) {
        String userName = getUsername();
        if (userName == null){
            return "error: session may have expired, please re-login";
        }
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        if (ss == null) {
            return "error: Failed to save annotations";
        }
        Permissions p = ss.getPermissions(userName);
        if (p.canRead()){
            SlideReference sr = new SlideReference(slideSetName, slideName, hash);
            String annos = ss.getAnnotationsXML(sr);
            return annos;
        } else {
            return "error: study does not exist or you do not have permission to modify it";
        }
    }

    @WebMethod(operationName = "setAnnotations")
    public String setAnnotations( @WebParam(name = "studyID") final int studyID,
                                  @WebParam(name = "slideSetName") final String slideSetName,
                                  @WebParam(name = "slideName") final String slideName,
                                  @WebParam(name = "hash") final byte[] hash,
                                  @WebParam(name = "annotations") final String annotations) {
        String userName = getUsername();
        if (userName == null){
            return "error: session may have expired, please re-login";
        }
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        if (ss == null) {
            return "error: Failed to save annotations";
        }
        Permissions p = ss.getPermissions(userName);
        if (p.canModify()) {
            SlideReference sr = new SlideReference(slideSetName, slideName, hash);
            ss.setAnnotationsXML(sr, annotations);
            return "Success";
        } else {
            return "error: study does not exist or you do not have permission to modify it";
        }
    }

    @WebMethod(operationName = "clearAnnotations")
    public String clearAnnotations( @WebParam(name = "studyID") final int studyID,
                                    @WebParam(name = "slideSetName") final String slideSetName,
                                    @WebParam(name = "slideName") final String slideName,
                                    @WebParam(name = "hash") final byte[] hash) {
        String userName = getUsername();
        if (userName == null){
            return "error: session may have expired, please re-login";
        }
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        if (ss == null) {
            return "error: Failed to save annotations";
        }
        Permissions p = ss.getPermissions(userName);
        if (p.canModify()) {
            SlideReference sr = new SlideReference(slideSetName, slideName, hash);
            ss.clearAnnotations(sr);
            return "Success";
        } else {
            return "error: study does not exist or you do not have permission to modify it";
        }
    }

    @WebMethod(operationName = "getSolution")
    public String getSolution( @WebParam(name = "solutionID") int id) {
        SolutionSourceKVStore sskvs = SolutionSourceKVStore.get(KeyValueRef.getRef(), id);
        Permissions p = sskvs.getPermissions(getUsername());
        if (p.canRead()){
            return sskvs.getSolutionXML();
        } else {
            return "error: permission denied";
        }
    }

    @WebMethod(operationName = "setSolution")
    public String setSolution( @WebParam(name = "solutionID") final int id, 
                               @WebParam(name = "xml") final String xml) {
        SolutionSourceKVStore sskvs = SolutionSourceKVStore.get(KeyValueRef.getRef(), id);
        if (sskvs == null) {
            return "errpr: solution does not exist";
        }
        Permissions p = sskvs.getPermissions(getUsername());
        if (p.canModify()){
            sskvs.setSolution(xml);
            return "Success";            
        } else {
            return "error: you do not have permission to modify this solution";
        }
    }

    @WebMethod(operationName = "getStudyVersion")
    public long getStudyVersion( @WebParam(name = "studyID") final int studyID) {
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        if (ss!= null){
            return ss.getVersion();
        }
        return -1;
    }

    @WebMethod(operationName = "getSolutionVersion")
    public long getSolutionVersion( @WebParam(name = "solutionID") final int solutionID) {
        SolutionSourceKVStore ss = SolutionSourceKVStore.get(KeyValueRef.getRef(), solutionID);
        if (ss!= null){
            return ss.getVersion();
        }
        return -1;
    }
    
    @WebMethod(operationName = "getSolutionsList")
    public String getSolutionsList( ) {
        ArrayList<PermissionsSet> permissionsSets = SolutionSourceKVStore.getPermissionsSets(KeyValueRef.getRef(),getUsername());
        StringBuilder sb = new StringBuilder();
        for (PermissionsSet ps:permissionsSets){
            String name = SolutionSourceKVStore.get(KeyValueRef.getRef(), ps.getID()).getName();
            sb.append(ps.getID()).append(",").append(name).append(";");
        }
        return sb.toString();
    }

    @WebMethod(operationName = "getSolutionsListVersion")
    public long getSolutionsListVersion( ) {
        return SolutionSourceKVStore.getPermissionsSetsVersion(KeyValueRef.getRef(),getUsername());
    }

    @WebMethod(operationName = "getStudiesList")
    public String getStudiesList(  ) {
        ArrayList<PermissionsSet> permissionsSets = StudySourceKVStore.getPermissionsSets(KeyValueRef.getRef(), getUsername());
        StringBuilder sb = new StringBuilder();
        for (PermissionsSet ps:permissionsSets){
            String name = StudySourceKVStore.get(KeyValueRef.getRef(), ps.getID()).getName();
            sb.append(ps.getID()).append(",").append(name).append(";");
        }
        return sb.toString();
    }

    @WebMethod(operationName = "getStudiesListVersion")
    public long getStudiesListVersion(  ) {
        // Returns a comma seperated list of classifier set file names
        return StudySourceKVStore.getPermissionsSetsVersion(KeyValueRef.getRef(), getUsername());
    }

    @WebMethod(operationName = "createStudy")
    public String createStudy( @WebParam(name = "studyName") final String studyName) {      
        StudySourceKVStore ss = StudySourceKVStore.create(KeyValueRef.getRef(), studyName, getUsername());
        if (ss == null) {
            return "error: Failed to create study";
        } else {
            return String.valueOf(ss.getId());
        }        
    }

    @WebMethod(operationName = "setStudyDescription")
    public String setStudyDescription( @WebParam(name = "studyID") final int studyID, 
                                       @WebParam(name = "studyDescription") final String studyDescription) {
        String userName = getUsername();
        if (userName == null){
            return "error: session may have expired, please re-login";
        }
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        if (ss == null) {
            return "error: Failed to set study description";
        }
        if (ss.getPermissions(userName).canModify()){
            return ss.setDescription(studyDescription);
        } else {
            return "error: permission denied by server";
        }
    }

    @WebMethod(operationName = "getStudyDescription")
    public String getStudyDescription( @WebParam(name = "studyID") final int studyID) {
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        String userName = getUsername();
        if (userName == null){
            return "error: session may have expired, please re-login";
        }
        if (ss.getPermissions(userName).canRead()){
            return ss.getDescription();
        } else {
            return "error: permission denied by server";
        }
    }

    @WebMethod(operationName = "createSolution")
    public String createSolution( @WebParam(name = "solutionName") final String solutionName, 
                                  @WebParam(name = "description") final String description) {
        SolutionSourceKVStore ss = SolutionSourceKVStore.create(KeyValueRef.getRef(), getUsername(), solutionName);
        if (ss == null) {
            return "error: Failed to create solution";
        } else {
            ss.setSolution(new Solution(solutionName));
            return String.valueOf(ss.getId());
        }
    }

    @WebMethod(operationName = "deleteSolution")
    public String deleteSolution( @WebParam(name = "solutionID") final int solutionID) {
        SolutionSourceKVStore ss = SolutionSourceKVStore.get(KeyValueRef.getRef(), solutionID);
        ss.setPermission(getUsername(), Permissions.DENIED);
        ss.delete();
        return "Success";
    }
    
    @WebMethod(operationName = "deleteStudy")
    public String deleteStudy( @WebParam(name = "studyID") final int studyID) {
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        ss.setPermission(getUsername(), Permissions.DENIED);
        ss.delete();
        return "Success";
    }

    @WebMethod(operationName = "getSlideSets")
    public String getSlideSets( @WebParam(name = "studyID") final int studyID) {
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        if (ss == null){
            return "error: study does not exist or you do not have permission to view it";
        }
        Permissions p = ss.getPermissions(getUsername());
        if (p.equals(Permissions.DENIED)) {
            return "error: study does not exist or you do not have permission to view it";
        }        
        SlideSets dataSets = ss.getSlideSets();
        String xml = SlideSetsXML_Writer.getXMLString(dataSets);
        return xml;
    }

    @WebMethod(operationName = "trainClassifier")
    public byte[] trainClassifier( @WebParam(name = "solutionID") final int id) {
        SolutionSourceKVStore solutionSource = SolutionSourceKVStore.get(KeyValueRef.getRef(), id);
        Permissions p = solutionSource.getPermissions(getUsername());
        if (!p.canModify()){
            return null;
        }
        Solution solution = solutionSource.getSolution();
        ArrayList<SlideSource> slideSources = collectRequiredSlideSources(solution);
        ArrayList<SampleSetClass> sampleSets = SampleSetClass.generateTrainingSets(slideSources, solution, null);
        solution.trainClassifiers(sampleSets);
        solutionSource.setSolution(solution);
        
        byte[] b = objectToByteArray(solution.getClassifiers());
        return b;
    }
    
    private byte[] objectToByteArray(Object obj) {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (XMLEncoder xmlEncoder = new XMLEncoder(buffer)) {
            xmlEncoder.writeObject(obj);
        }
        byte[] b = buffer.toByteArray();
        return b;
    }

    private static ArrayList<SlideSource> collectRequiredSlideSources(Solution solution) {
        ArrayList<SlideSource> slideSources = new ArrayList<>();
        for (ClassifierClass c : solution.getClasses()) {
            for (AnnotationSet annoSet : c.getAnnotationSets()) {
                boolean alreadyContainsSource = false;
                for (SlideSource slideSource : slideSources) {
                    if (ImageID.hashesAreEqual(slideSource.getHash(), annoSet.getHash())) {
                        // If set already has this slide;
                        alreadyContainsSource = true;
                        break;
                    }
                }
                if (alreadyContainsSource){
                    continue;
                }
                SlideSourceKVStore slide = new SlideSourceKVStore(annoSet.getSlideReference(), KeyValueRef.getRef());
                //TODO: check hashs
                slideSources.add(slide);
            }
        }
        return slideSources;
    }

    @WebMethod(operationName = "setPermissionsSolution")
    public String setPermissionsSolution(   @WebParam(name = "solutionID") final int solutionID,
                                            @WebParam(name = "username") final String targetUsername,
                                            @WebParam(name = "permission") final String newPermission){       
        SolutionSourceKVStore ss = SolutionSourceKVStore.get(KeyValueRef.getRef(), solutionID);
        if (ss == null){
            return "error: solution does not exist";
        }
        if (ss.getPermissions(getUsername()).equals(Permissions.ADMIN)){
            ss.setPermission(targetUsername, Permissions.valueOf(newPermission));
            return "Permission set";
        } else {
            return "error: Permission change denied by server";
        }
    }    
    
    @WebMethod(operationName = "setPermissionsStudy")
    public String setPermissionsStudy(  @WebParam(name = "studyID") final int studyID,
                                        @WebParam(name = "username") final String targetUsername,
                                        @WebParam(name = "permission") final String newPermission){       
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        if (ss == null){
            return "error: solution does not exist";
        }
        if (ss.getPermissions(getUsername()).equals(Permissions.ADMIN)){
            ss.setPermission(targetUsername, Permissions.valueOf(newPermission));
            return "Permission set";
        } else {
            return "error: Permission change denied by server";
        }
    }   
    
       
    @WebMethod(operationName = "getPermissionsStudy")
    public String getPermissionsStudy(  @WebParam(name = "studyID") final int studyID, 
                                        @WebParam(name = "targerUsername") String targerUsername){       
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        if (ss == null){
            return "error: solution does not exist";
        }
        if ((targerUsername == null) || (targerUsername.isEmpty())){
            targerUsername = getUsername();
        }
        Permissions p = ss.getPermissions(targerUsername);
        return p.name();     
    } 
    
    @WebMethod(operationName = "getPermissionsSolution")
    public String getPermissionsSolution( @WebParam(name = "solutionID") final int solutionID, 
                                          @WebParam(name = "targerUsername") String targerUsername){       
        SolutionSourceKVStore ss = SolutionSourceKVStore.get(KeyValueRef.getRef(), solutionID);
        if (ss == null){
            return "error: solution does not exist";
        }
        if ((targerUsername == null) || (targerUsername.isEmpty())){
            targerUsername = getUsername();
        }
        Permissions p = ss.getPermissions(targerUsername);
        return p.name();          
    }   
        
    @WebMethod(operationName = "getUsersInStudy")
    public String getUsersInStudy( @WebParam(name = "studyID") final int studyID){
        StudySourceKVStore ss = StudySourceKVStore.get(KeyValueRef.getRef(), studyID);
        String userName = getUsername();
        Permissions p = ss.getPermissions(userName);
        if (p.canRead()){
            return ss.getUserList();
        } else {
            return "error: permission denied to view users";
        }
    }
       
    @WebMethod(operationName = "getUsersInSolution")
    public String getUsersInSolution( @WebParam(name = "solutionID") final int solutionID){
        SolutionSourceKVStore ss = SolutionSourceKVStore.get(KeyValueRef.getRef(), solutionID);
        Permissions p = ss.getPermissions(getUsername());
        if (p.canRead()){
            return ss.getUserList();
        } else {
            return "error: permission denied to view users";
        }
    }
    
    @WebMethod(operationName = "login")
    public String login( @WebParam(name = "username") final String username,
                         @WebParam(name = "password") final String password ) {
        if (KeyValueRef.getRef() == null){
            return "error: the server cannot connect to it's database";
        }
        return sessionManager.login(getSessionID(), username.toLowerCase(), password);
    }
        
//    private boolean isAdmin( String username ){
//        ArrayList<String> major = new ArrayList<>();
//        major.add("user");
//        major.add(username);
//        ArrayList<String> minor = new ArrayList<>();
//        minor.add("isAdmin");
//        ValueVersion vv = KeyValueRef.getRef().get(Key.createKey(major, minor));
//        return vv != null;
//    }    
    
    private String getUsername(){
        return sessionManager.getUsername(getSessionID());
    }
    
    @WebMethod(operationName = "keepAlive")
    public String keepAlive() {
        if (getUsername() == null){
            return "error: user session expired";
        } else {
            return "Success";
        }
    }

    @WebMethod(operationName = "registerUser")
    public String registerUser( @WebParam(name = "username") final String username, 
                                @WebParam(name = "password") final String password) {
        KVStore kvstore = KeyValueRef.getRef();
        try {           
            ArrayList<String> major = new ArrayList<>();
            major.add("user");
            kvstore.putIfAbsent(Key.createKey(major), Value.EMPTY_VALUE);
            major.add(username.toLowerCase());
            
            ValueVersion vv = kvstore.get(Key.createKey(major));
            if (vv != null){
                return "error: username already exists";
            }
            
            kvstore.putIfAbsent(Key.createKey(major), Value.EMPTY_VALUE);
            ArrayList<String> minor = new ArrayList<>();
            minor.add("password");   
                        
            String hash = PasswordHash.createHash(password);
            kvstore.put(Key.createKey(major, minor), Value.createValue(hash.getBytes()));
                        
        } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
            Logger.getLogger(Training.class.getName()).log(Level.SEVERE, null, ex);
            return "error: something failed on the server while registering user " + username;
        }
        return "user created";
    }
    
}
